Skip to content

[WC-3361] FileUploader: enhance limits and feedback#2208

Open
yordan-st wants to merge 9 commits into
mainfrom
fix/WC-3361_fileuploader-limit-feedback
Open

[WC-3361] FileUploader: enhance limits and feedback#2208
yordan-st wants to merge 9 commits into
mainfrom
fix/WC-3361_fileuploader-limit-feedback

Conversation

@yordan-st
Copy link
Copy Markdown
Contributor

@yordan-st yordan-st commented May 11, 2026

Pull request type

Bug fix (non-breaking change which fixes an issue)


Description

When the file upload limit was reached, the dropzone silently turned grey with no explanation to the user. Dropping more files than the limit allowed rejected the entire batch, and files stuck in error state had no way to recover.

IMPORTANT NOTE: we need to merge [WC-3363 Fileuploader: dismiss action for validation errors](#2198 (comment)) first

This PR addresses everything below:

  • Silent disabled dropzone -> now shows "Maximum file count of X reached."
  • Drop overflow now splits on remaining capacity. Only the excess are shown as errors
  • Files rejected due to total or batch limit auto-retry when capacity is freed (file removed or error dismissed)
  • Wrong XML description on maxFilesPerUpload fixed
  • No way to set unlimited -> required="false" added; 0 and empty both mean unlimited (default)
  • New "Maximum files per upload batch" property to cap server commits per drop event
  • New "File limit reached" and "Batch limit exceeded" text properties for message customization
  • File list reordered: successful uploads shown above rejected files

What should be covered while testing?

Setup: File Uploader, "Maximum number of files" = 5, "Maximum files per upload batch" = 2, files mode.

File limit feedback:

  1. Upload 1 file → dropzone stays active, no warning shown
  2. Upload until total = limit → dropzone turns grey AND message appears: "Maximum file count of 5 reached."
  3. Remove 1 file while at limit → dropzone re-enables, message disappears
  4. Refresh page with existing files at limit → dropzone immediately grey with message on load, no user action needed

Unlimited behavior:
5. Set total limit = 0 → dropzone never disables regardless of file count
6. Leave total limit empty → same as 0, unlimited

Drop total limit:
7. Set total limit = 3, no batch limit. Drop 5 files → first 3 upload, last 2 appear as errors with "Maximum file count of 3 reached."
8. Remove 1 uploaded file → one of the errored files automatically starts uploading

Batch limit:
9. Set batch limit = 2, drop 5 files → first 2 upload, remaining 3 appear as errors: "File not uploaded. Batch limit of 2 files per drop was reached."
10. Remove 1 uploaded file → one of the batch-errored files automatically starts uploading (respects batch limit, max 2 retry at once)
11. Leave batch limit empty → all dropped files upload regardless of count per drop

List ordering:
12. Upload a mix of valid and invalid files → successful uploads appear above rejected files in the list

Other:
13. Read-only mode → dropzone not rendered, no regression
14. Image upload mode → same limit-reached and retry behavior applies

@yordan-st yordan-st force-pushed the fix/WC-3361_fileuploader-limit-feedback branch from 430b80b to 4d8b53f Compare May 11, 2026 15:02
@yordan-st yordan-st marked this pull request as ready for review May 12, 2026 13:00
@yordan-st yordan-st requested a review from a team as a code owner May 12, 2026 13:00
@yordan-st yordan-st force-pushed the fix/WC-3361_fileuploader-limit-feedback branch from a444b83 to 7d054a3 Compare May 12, 2026 13:01
@github-actions

This comment was marked as outdated.

@github-actions

This comment was marked as outdated.

@yordan-st yordan-st force-pushed the fix/WC-3361_fileuploader-limit-feedback branch from 9fe41dd to 09489e2 Compare May 12, 2026 13:45
@github-actions

This comment was marked as outdated.

@github-actions
Copy link
Copy Markdown

AI Code Review

⚠️ Approved with suggestions — low-severity items only, safe to merge


What was reviewed

File Change
packages/pluggableWidgets/file-uploader-web/CHANGELOG.md New Unreleased entries for all changes
packages/pluggableWidgets/file-uploader-web/src/FileUploader.xml Added maxFilesPerBatch, uploadLimitReachedMessage, uploadBatchLimitExceededMessage; made maxFilesPerUpload optional
packages/pluggableWidgets/file-uploader-web/typings/FileUploaderProps.d.ts New optional props in Container and Preview interfaces
packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts Core logic: batch/capacity split, retryLimitExceededFiles, sortedFiles, warningMessage, MobX reaction for auto-retry, dispose()
packages/pluggableWidgets/file-uploader-web/src/stores/FileStore.ts errorType field, reset() method, updated markError signature
packages/pluggableWidgets/file-uploader-web/src/stores/TranslationsStore.ts Import reorder only
packages/pluggableWidgets/file-uploader-web/src/components/Dropzone.tsx Removed maxFilesPerUpload prop (limit enforcement moved to store)
packages/pluggableWidgets/file-uploader-web/src/components/FileUploaderRoot.tsx Uses warningMessage and sortedFiles
packages/pluggableWidgets/file-uploader-web/src/utils/useRootStore.ts Adds useEffect cleanup calling dispose()
packages/pluggableWidgets/file-uploader-web/src/stores/__tests__/FileUploaderStore.spec.ts New test file covering warningMessage, isFileUploadLimitReached, processDrop, retryLimitExceededFiles
Other *.tsx / *.ts files Import-order reordering only (prettier/eslint)

Skipped (out of scope): dist/, pnpm-lock.yaml


Findings

⚠️ Low — Missing store.dispose() in test teardown leaks MobX reaction

File: packages/pluggableWidgets/file-uploader-web/src/stores/__tests__/FileUploaderStore.spec.ts (whole file)
Problem: buildStore() sets up a MobX reaction that is never cleaned up in tests. Without an afterEach calling store.dispose(), the reaction keeps running across tests and can fire unexpectedly, causing test pollution (especially the splice-triggered tests that rely on counting reaction invocations).
Fix:

describe("FileUploaderStore.warningMessage", () => {
    let store: FileUploaderStore;
    afterEach(() => store.dispose());
    // ... pass store ref from each test
});

Or add a shared afterEach at the top level:

const stores: FileUploaderStore[] = [];
afterEach(() => { stores.forEach(s => s.dispose()); stores.length = 0; });
// in buildStore: const s = new FileUploaderStore(...); stores.push(s); return s;

⚠️ Low — Manual as any stub objects instead of FileStore instances in tests

File: packages/pluggableWidgets/file-uploader-web/src/stores/__tests__/FileUploaderStore.spec.ts lines 78, 96, 114, 125, 134, 153, 167, 180
Problem: Tests push plain { fileStatus: "existingFile" } as any objects directly into store.files. These bypass MobX observability (makeObservable is never called on them), so changes to fileStatus won't trigger computed recalculations in isFileUploadLimitReached/warningMessage. Tests like the splice-based retry tests succeed only because retryLimitExceededFiles is called explicitly via the reaction, not because the computed value reacted to a stub.
Fix: Use FileStore.existingFile(obj("a"), store) (the factory already exists) for active-file placeholders, and FileStore.newFileWithError(file, msg, store, "limitExceeded") for waiting files, so MobX observability is exercised end-to-end.


⚠️ Low — retryLimitExceededFiles uses Number.MAX_SAFE_INTEGER as unlimited sentinel

File: packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts line 203
Problem: Number.MAX_SAFE_INTEGER (~9 × 10¹⁵) is passed into Math.min(capacitySlots, maxFilesPerBatch). While harmless in practice (the waiting.length guard caps the loop), it's an unusual sentinel. A simple Infinity communicates "unlimited" more clearly and is idiomatic for this pattern.
Fix:

const capacitySlots =
    this.maxFilesPerUpload > 0 ? Math.max(0, this.maxFilesPerUpload - activeCount) : Infinity;
const slots = this.maxFilesPerBatch > 0 ? Math.min(capacitySlots, this.maxFilesPerBatch) : capacitySlots;

⚠️ Low — uploadFailureTooManyFilesMessage XML property is now unreferenced in source

File: packages/pluggableWidgets/file-uploader-web/src/FileUploader.xml line 163 (unchanged)
Problem: The old uploadFailureTooManyFilesMessage textTemplate is still in the XML and TypeScript typings but is never read by any store or component after this PR (only referenced in the test's buildProps helper). It appears to be dead UI surface. Leaving it in exposes an unused configuration field to Mendix app developers.
Fix: Determine if this property should be deprecated or removed. If the uploadLimitReachedMessage fully replaces it, add a Studio Pro deprecation note and plan removal in the next major version. At minimum, add a comment in the XML explaining the relationship.


Positives

  • dispose() is correctly wired up with a useEffect cleanup in useRootStore.ts — the MobX reaction will not leak in production.
  • Capacity split logic in processDrop cleanly separates batch-excess from capacity-excess in a single pass, avoiding multiple scans of the file list.
  • sortedFiles computed getter sorts in-place on a copy ([...this.files]), correctly avoiding mutation of the observable array.
  • CHANGELOG entries are thorough — all three changed surfaces (Fixed, Added, Changed) are documented under [Unreleased].
  • XML property keys follow lowerCamelCase convention and TypeScript typings are kept in sync.

}
}
);
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks really cumbersome to have essentially upload-throttling mechanism implemented like this. If we want upload to happen automatically in batches, we should make a proper uploading logic that keeps files in the queue and uploads max X files at a time and files should be in a queueing state, not in the error/warning state then.

@@ -116,11 +140,18 @@ export class FileUploaderStore {
}

get maxFilesPerUpload(): number {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is still called PerUpload, this is confusing, rename it to better match the purpose

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

THe XML we can't rename unfortunately, but this we def can

yordan-st added 9 commits May 18, 2026 14:57
- Show "Maximum file count of X reached" message when dropzone is disabled
- Make maxFilesPerUpload optional (empty/0 = unlimited)
- Add maxFilesPerBatch property to cap server commits per drop event
- Split drops by remaining capacity — excess files shown as errors, not silently rejected
- Auto-retry limit/batch-exceeded files when capacity is freed
- Batch/limit-exceeded files survive dismissValidationErrors and re-queue correctly
- Retry order matches visual list order (newest first)
- Reorder file list: accepted files above rejected files
@yordan-st yordan-st force-pushed the fix/WC-3361_fileuploader-limit-feedback branch from d06abf7 to 8fe5f51 Compare May 18, 2026 12:57
@github-actions
Copy link
Copy Markdown

AI Code Review

🔶 Changes requested — one or more medium-severity items must be addressed


What was reviewed

File Change
src/stores/FileUploaderStore.ts New maxFilesPerBatch, sortedFiles, warningMessage, retryLimitExceededFiles, MobX reaction for auto-retry
src/stores/FileStore.ts New errorType field; reset() action; markError now accepts error type
src/stores/__tests__/FileUploaderStore.spec.ts New test file — 253 lines covering new store behaviours
src/FileUploader.editorConfig.ts Import reorder only
src/FileUploader.editorPreview.tsx Import reorder only
src/FileUploader.xml maxFilesPerUpload made optional; new maxFilesPerBatch, uploadLimitReachedMessage, uploadBatchLimitExceededMessage
src/components/Dropzone.tsx maxFilesPerUpload prop removed (limit enforcement moved to store)
src/components/FileUploaderRoot.tsx Uses rootStore.sortedFiles and rootStore.warningMessage
src/utils/useRootStore.ts Adds dispose() cleanup on unmount
typings/FileUploaderProps.d.ts maxFilesPerUpload optional; maxFilesPerBatch, uploadLimitReachedMessage, uploadBatchLimitExceededMessage added
CHANGELOG.md Unreleased section updated

Skipped (out of scope): dist/, pnpm-lock.yaml

⚠️ CI check results could not be retrieved automatically — verify checks are green before merging.


Findings

🔶 Medium — maxFilesPerBatch not hidden in read-only mode in editorConfig

File: src/FileUploader.editorConfig.ts lines 22–26
Problem: When readOnlyMode is true, the editor config hides maxFilesPerUpload, maxFileSize, and objectCreationTimeout — all upload-time properties. The new maxFilesPerBatch property is upload-time too but is absent from this list, so Studio Pro will show it to users configuring a read-only widget, creating a confusing no-op setting.
Fix:

hidePropertiesIn(properties, values, [
    values.uploadMode === "files" ? "associatedImages" : "associatedFiles",
    "createImageAction",
    "createFileAction",
    "allowedFileFormats",
    "maxFilesPerUpload",
    "maxFilesPerBatch",   // ← add this
    "maxFileSize",
    "objectCreationTimeout"
]);

⚠️ Low — Tests push plain objects into store.files instead of using FileStore factories

File: src/stores/__tests__/FileUploaderStore.spec.ts lines 77–80, 96–99, 113–116, 132–138
Note: These tests cast raw objects ({ fileStatus: "existingFile" } as any) and push them into the MobX observable array. This bypasses the FileStore constructor and makeObservable call, so the pushed items are plain non-observable objects. It works for current assertions (count-based filtering), but breaks down for any future test that needs fileStatus changes on those objects to be reactive, and is fragile against FileStore structural refactors. Using the existing static factories is straightforward:

// instead of:
store.files.push({ fileStatus: "existingFile", _objectItem: obj("a") } as any);

// use:
const fileA = FileStore.existingFile(obj("a"), store);
store.files.push(fileA);

FileStore.existingFile calls fetchMxObject asynchronously — stub mx-data at the top of the test file if needed (as the DatasourceUpdateProcessor.spec.ts already does for the same module).


Positives

  • dispose() method added to FileUploaderStore and wired up in useRootStore via a cleanup useEffect — the MobX reaction no longer leaks on widget unmount.
  • New errorType discriminant on FileStore is properly registered as observable in makeObservable and correctly updated in all three creation paths (newFileWithError, markError, and the constructor).
  • Moving limit enforcement from react-dropzone's maxFiles option into the store is the right call — it eliminates the silent all-or-nothing rejection and gives fine-grained control over which files pass vs. error.
  • retryLimitExceededFiles is registered as a MobX action, so all state mutations inside it are batched and the retry reaction won't cascade.
  • CHANGELOG entry is thorough and covers fixed, added, and changed categories with user-facing language.
  • New XML properties use lowerCamelCase keys (maxFilesPerBatch, uploadLimitReachedMessage, uploadBatchLimitExceededMessage) consistent with the repo convention, and TypeScript typings are kept in sync.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants